/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.gen;

import static com.android.dx.rop.code.AccessFlags.ACC_CONSTRUCTOR;
import static com.android.dx.rop.code.AccessFlags.ACC_PRIVATE;
import static com.android.dx.rop.code.AccessFlags.ACC_STATIC;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;

import com.android.dx.dex.DexFormat;
import com.android.dx.dex.DexOptions;
import com.android.dx.dex.code.DalvCode;
import com.android.dx.dex.code.PositionList;
import com.android.dx.dex.code.RopTranslator;
import com.android.dx.dex.file.ClassDefItem;
import com.android.dx.dex.file.DexFile;
import com.android.dx.dex.file.EncodedField;
import com.android.dx.dex.file.EncodedMethod;
import com.android.dx.rop.code.AccessFlags;
import com.android.dx.rop.code.LocalVariableInfo;
import com.android.dx.rop.code.RopMethod;
import com.android.dx.rop.cst.CstString;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.type.StdTypeList;

/**
 * Define types, fields and methods.
 */
public final class DexGenerator {

	private final Map<Type<?>, TypeDeclaration> types = new LinkedHashMap<Type<?>, TypeDeclaration>();

	private TypeDeclaration getTypeDeclaration(Type<?> type) {
		TypeDeclaration result = types.get(type);
		if (result == null) {
			result = new TypeDeclaration(type);
			types.put(type, result);
		}
		return result;
	}

	/**
	 * @param flags
	 *            any flags masked by
	 *            {@link com.android.dx.rop.code.AccessFlags#CLASS_FLAGS}.
	 */
	public void declare(Type<?> type, String sourceFile, int flags,
			Type<?> supertype, Type<?>... interfaces) {
		TypeDeclaration declaration = getTypeDeclaration(type);
		if (declaration.declared) {
			throw new IllegalStateException("already declared: " + type);
		}
		declaration.declared = true;
		declaration.flags = flags;
		declaration.supertype = supertype;
		declaration.sourceFile = sourceFile;
		declaration.interfaces = new TypeList(interfaces);
	}

	/**
	 * @param flags
	 *            any flags masked by
	 *            {@link com.android.dx.rop.code.AccessFlags#METHOD_FLAGS}.
	 */
	public Code declare(MethodId<?, ?> method, int flags) {
		TypeDeclaration typeDeclaration = getTypeDeclaration(method.declaringType);
		if (typeDeclaration.methods.containsKey(method)) {
			throw new IllegalStateException("already declared: " + method);
		}
		MethodDeclaration methodDeclaration = new MethodDeclaration(method,
				flags);
		typeDeclaration.methods.put(method, methodDeclaration);
		return methodDeclaration.code;
	}

	/**
	 * @param flags
	 *            any flags masked by {@link AccessFlags#FIELD_FLAGS}.
	 */
	public void declare(FieldId<?, ?> fieldId, int flags, Object staticValue) {
		TypeDeclaration typeDeclaration = getTypeDeclaration(fieldId.declaringType);
		if (typeDeclaration.fields.containsKey(fieldId)) {
			throw new IllegalStateException("already declared: " + fieldId);
		}
		FieldDeclaration fieldDeclaration = new FieldDeclaration(fieldId,
				flags, staticValue);
		typeDeclaration.fields.put(fieldId, fieldDeclaration);
	}

	/**
	 * Returns a .dex formatted file.
	 */
	public byte[] generate() {
		DexFile outputDex = new DexFile(new DexOptions());

		for (TypeDeclaration typeDeclaration : types.values()) {
			outputDex.add(typeDeclaration.toClassDefItem());
		}

		try {
			return outputDex.toDex(null, false);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Loads the generated types into the current dalvikvm process.
	 */
	public ClassLoader load(ClassLoader parent) throws IOException {
		byte[] dex = generate();

		/*
		 * This implementation currently dumps the dex to the filesystem. It
		 * jars the emitted .dex for the benefit of Gingerbread and earlier
		 * devices, which can't load .dex files directly.
		 * 
		 * TODO: load the dex from memory where supported.
		 */
		File result = File.createTempFile("Generated", ".jar");
		result.deleteOnExit();
		JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(
				result));
		jarOut.putNextEntry(new JarEntry(DexFormat.DEX_IN_JAR_NAME));
		jarOut.write(dex);
		jarOut.closeEntry();
		jarOut.close();
		try {
			Class<?> pathClassLoader = Class
					.forName("dalvik.system.PathClassLoader");
			return (ClassLoader) pathClassLoader.getConstructor(String.class,
					ClassLoader.class).newInstance(result.getPath(), parent);
		} catch (ClassNotFoundException e) {
			throw new UnsupportedOperationException(
					"load() requires a Dalvik VM", e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e.getCause());
		} catch (InstantiationException e) {
			throw new AssertionError();
		} catch (NoSuchMethodException e) {
			throw new AssertionError();
		} catch (IllegalAccessException e) {
			throw new AssertionError();
		}
	}

	private static class TypeDeclaration {

		private final Type<?> type;

		/** declared state */
		private boolean declared;
		private int flags;
		private Type<?> supertype;
		private String sourceFile;
		private TypeList interfaces;

		private final Map<FieldId, FieldDeclaration> fields = new LinkedHashMap<FieldId, FieldDeclaration>();
		private final Map<MethodId, MethodDeclaration> methods = new LinkedHashMap<MethodId, MethodDeclaration>();

		TypeDeclaration(Type<?> type) {
			this.type = type;
		}

		ClassDefItem toClassDefItem() {
			if (!declared) {
				throw new IllegalStateException("Undeclared type " + type
						+ " declares members: " + fields.keySet() + " "
						+ methods.keySet());
			}

			DexOptions dexOptions = new DexOptions();
			dexOptions.targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;

			CstType thisType = type.constant;

			ClassDefItem out = new ClassDefItem(thisType, flags,
					supertype.constant, interfaces.ropTypes, new CstString(
							sourceFile));

			for (MethodDeclaration method : methods.values()) {
				EncodedMethod encoded = method.toEncodedMethod(dexOptions);
				if (method.isDirect()) {
					out.addDirectMethod(encoded);
				} else {
					out.addVirtualMethod(encoded);
				}
			}
			for (FieldDeclaration field : fields.values()) {
				EncodedField encoded = field.toEncodedField();
				if (field.isStatic()) {
					out.addStaticField(encoded,
							Constants.getConstant(field.staticValue));
				} else {
					out.addInstanceField(encoded);
				}
			}

			return out;
		}
	}

	static class FieldDeclaration {

		final FieldId<?, ?> fieldId;
		private final int accessFlags;
		private final Object staticValue;

		FieldDeclaration(FieldId<?, ?> fieldId, int accessFlags,
				Object staticValue) {
			if ((accessFlags & (AccessFlags.ACC_STATIC)) == 0
					&& staticValue != null) {
				throw new IllegalArgumentException(
						"instance fields may not have a value");
			}
			this.fieldId = fieldId;
			this.accessFlags = accessFlags;
			this.staticValue = staticValue;
		}

		EncodedField toEncodedField() {
			return new EncodedField(fieldId.constant, accessFlags);
		}

		public boolean isStatic() {
			return (accessFlags & (AccessFlags.ACC_STATIC)) != 0;
		}
	}

	static class MethodDeclaration {

		final MethodId<?, ?> method;
		private final int flags;
		private final Code code;

		public MethodDeclaration(MethodId<?, ?> method, int flags) {
			this.method = method;
			this.flags = flags;
			this.code = new Code(this);
		}

		boolean isStatic() {
			return (flags & ACC_STATIC) != 0;
		}

		boolean isDirect() {
			return (flags & (ACC_STATIC | ACC_PRIVATE | ACC_CONSTRUCTOR)) != 0;
		}

		EncodedMethod toEncodedMethod(DexOptions dexOptions) {
			RopMethod ropMethod = new RopMethod(code.toBasicBlocks(), 0);
			LocalVariableInfo locals = null;
			DalvCode dalvCode = RopTranslator.translate(ropMethod,
					PositionList.NONE, locals, code.paramSize(), dexOptions);
			return new EncodedMethod(method.constant, flags, dalvCode,
					StdTypeList.EMPTY);
		}
	}
}
